Skip to content

fix: eliminate FOUC on cookie accept and custom font load#4999

Open
woohyeokk-choi wants to merge 1 commit intofastrepl:mainfrom
woohyeokk-choi:fix/fouc-posthog-font-display
Open

fix: eliminate FOUC on cookie accept and custom font load#4999
woohyeokk-choi wants to merge 1 commit intofastrepl:mainfrom
woohyeokk-choi:fix/fouc-posthog-font-display

Conversation

@woohyeokk-choi
Copy link
Copy Markdown
Contributor

@woohyeokk-choi woohyeokk-choi commented Apr 11, 2026

Summary

  • Remove isInitialized state from PostHogProvider so the React wrapper type stays stable (PostHogReactProvider in prod with an API key). Previously, accepting cookie consent triggered a Fragment → PostHogReactProvider wrapper swap that unmounted and remounted the entire page, causing a brief flash of unstyled content.
  • Add font-display: swap to all @font-face rules (Redaction, SF Pro) so custom fonts swap in without blocking render.

Root cause

PostHogProvider conditionally rendered <>{children}</> or <PostHogReactProvider> based on an isInitialized state that started as false. On consent accept, React saw a component type change and remounted all children — visible as FOUC.

Safety verification

Verified against posthog-js v1.367.0 source: all capture() / identify() calls are silent no-ops before init() via the !this.__loaded guard. Rendering PostHogReactProvider with an uninitialized client sets no cookies, makes no network requests, and queues no events.

Test plan

  • Accept cookie consent on char.com — no FOUC flash
  • Reject cookie consent — page reloads cleanly (existing behavior unchanged)
  • PostHog events still fire after accepting consent
  • Custom fonts (Redaction, SF Pro) render without layout shift

🤖 Generated with Claude Code


Note

Low Risk
Low risk UI/perf changes: adjusts PostHog wrapper rendering and font loading behavior, with limited blast radius beyond analytics initialization timing.

Overview
Prevents a full subtree remount (and resulting FOUC) when analytics consent is toggled by making PostHogProvider always render PostHogReactProvider in production when an API key is present, while guarding posthog.init() to run only once via didInitRef.

Improves perceived loading by adding font-display: swap to all custom @font-face declarations so text renders immediately with fallback fonts and swaps when Redaction/SF Pro are ready.

Reviewed by Cursor Bugbot for commit 5258d01. Bugbot is set up for automated code reviews on this repo. Configure here.

Remove the isInitialized state from PostHogProvider so the wrapper
type stays stable (always PostHogReactProvider in prod) and React
never unmounts/remounts children when analytics consent is given.
Verified safe against posthog-js v1.367.0 source: all capture/identify
calls are no-ops before init() via the __loaded guard.

Add font-display: swap to all @font-face rules so custom fonts
(Redaction, SF Pro) swap in without blocking render.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify bot commented Apr 11, 2026

👷 Deploy request for hyprnote pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 5258d01

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 11, 2026

Deploy Preview for char-cli-web canceled.

Name Link
🔨 Latest commit 5258d01
🔍 Latest deploy log https://app.netlify.com/projects/char-cli-web/deploys/69da146e73719b000855aea2

Copy link
Copy Markdown
Collaborator

@ComputelessComputer ComputelessComputer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FOUC fix is correct in diagnosis and the font change is fine to ship. One issue with the PostHog change worth addressing before merge.

The problem: removing the isInitialized wrapper and always rendering PostHogReactProvider fixes the remount flash, but it silently drops mount-time analytics events for returning users with stored consent. The old Fragment → PostHogReactProvider swap was ugly but accidentally gave components a second mount cycle after init — meaning hero_section_viewed, identify on auth callback, and identify on billing refresh all had a chance to fire against an initialized client. Now they fire once, before posthog.init() runs, and are lost.

On compliance: the banner exists to comply with GDPR/CCPA. Always rendering PostHogReactProvider before init() is technically safe from a cookie/data perspective (posthog-js is inert before init()), but the spirit of the consent gate is that no tracking happens before the user accepts. Queuing events that fire before consent and then replaying them after the user accepts would silently track users retroactively — which is the behavior GDPR specifically prohibits. So events that fire before consent should stay dropped.

Suggested fix:

  1. Keep the stable wrapper (always render PostHogReactProvider) — fixes FOUC ✅
  2. Keep posthog.init() gated on consent — GDPR/CCPA compliant ✅
  3. For the data loss issue: guard mount-time tracking calls with an analyticsReady check (not just posthog exists), so they only fire after init. Don't replay — just accept that events before consent are intentionally not collected.

The font font-display: swap change is clean and unrelated — happy to see that ship.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants